之前有參加 Elixir Taiwan 的社群,有一個專案 demo 是抓取股票相關資訊的專案,
想說這週都寫跟資料有關的,不如我也來搞一個股票追蹤器,這裡我們開始嘗試做一個
股票追蹤器抓取股票相關資料,以及針對一些指標進行分析
簡單概述 : 從公開 API 抓取股價資料,並計算常見的技術指標如移動平均線(MA)、相對強弱指標(RSI)等
cargo new stock_tracker
cd stock_tracker
cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["json", "blocking"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
資料結構
use serde::{Deserialize, Serialize};
use chrono::NaiveDate;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StockPrice {
pub date: NaiveDate,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: u64,
}
#[derive(Debug, Clone)]
pub struct TechnicalIndicators {
pub sma_20: Option<f64>, // 20日簡單移動平均
pub sma_50: Option<f64>, // 50日簡單移動平均
pub rsi_14: Option<f64>, // 14日相對強弱指標
pub ema_12: Option<f64>, // 12日指數移動平均
}
#[derive(Debug)]
pub struct StockData {
pub symbol: String,
pub prices: Vec<StockPrice>,
}
實作資料抓取模組
use reqwest;
use anyhow::{Result, Context};
pub struct StockFetcher {
client: reqwest::Client,
api_key: String,
}
impl StockFetcher {
pub fn new(api_key: String) -> Self {
Self {
client: reqwest::Client::new(),
api_key,
}
}
pub async fn fetch_stock_data(
&self,
symbol: &str,
days: usize,
) -> Result<StockData> {
// 這裡使用 Alpha Vantage API 作為範例
let url = format!(
"https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol={}&apikey={}&outputsize=compact",
symbol, self.api_key
);
let response = self.client
.get(&url)
.send()
.await
.context("Failed to fetch stock data")?;
let data: serde_json::Value = response
.json()
.await
.context("Failed to parse JSON")?;
self.parse_stock_data(symbol, data, days)
}
fn parse_stock_data(
&self,
symbol: &str,
data: serde_json::Value,
days: usize,
) -> Result<StockData> {
let time_series = data["Time Series (Daily)"]
.as_object()
.context("Invalid response format")?;
let mut prices: Vec<StockPrice> = time_series
.iter()
.take(days)
.filter_map(|(date_str, values)| {
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d").ok()?;
Some(StockPrice {
date,
open: values["1. open"].as_str()?.parse().ok()?,
high: values["2. high"].as_str()?.parse().ok()?,
low: values["3. low"].as_str()?.parse().ok()?,
close: values["4. close"].as_str()?.parse().ok()?,
volume: values["5. volume"].as_str()?.parse().ok()?,
})
})
.collect();
// 按日期排序(由舊到新)
prices.sort_by(|a, b| a.date.cmp(&b.date));
Ok(StockData {
symbol: symbol.to_string(),
prices,
})
}
}
建立指標計算模組
pub struct IndicatorCalculator;
impl IndicatorCalculator {
/// 計算簡單移動平均線 (SMA)
pub fn calculate_sma(prices: &[f64], period: usize) -> Vec<Option<f64>> {
let mut result = Vec::with_capacity(prices.len());
for i in 0..prices.len() {
if i + 1 < period {
result.push(None);
} else {
let sum: f64 = prices[i + 1 - period..=i].iter().sum();
result.push(Some(sum / period as f64));
}
}
result
}
/// 計算指數移動平均線 (EMA)
pub fn calculate_ema(prices: &[f64], period: usize) -> Vec<Option<f64>> {
if prices.is_empty() {
return vec![];
}
let mut result = Vec::with_capacity(prices.len());
let multiplier = 2.0 / (period as f64 + 1.0);
// 第一個 EMA 使用 SMA
let mut ema = prices.iter().take(period).sum::<f64>() / period as f64;
for (i, &price) in prices.iter().enumerate() {
if i < period - 1 {
result.push(None);
} else if i == period - 1 {
result.push(Some(ema));
} else {
ema = (price - ema) * multiplier + ema;
result.push(Some(ema));
}
}
result
}
/// 計算相對強弱指標 (RSI)
pub fn calculate_rsi(prices: &[f64], period: usize) -> Vec<Option<f64>> {
if prices.len() < period + 1 {
return vec![None; prices.len()];
}
let mut result = Vec::with_capacity(prices.len());
let mut gains = Vec::new();
let mut losses = Vec::new();
// 計算價格變化
for i in 1..prices.len() {
let change = prices[i] - prices[i - 1];
if change > 0.0 {
gains.push(change);
losses.push(0.0);
} else {
gains.push(0.0);
losses.push(change.abs());
}
}
result.push(None); // 第一天沒有 RSI
for i in 0..gains.len() {
if i < period - 1 {
result.push(None);
} else {
let avg_gain: f64 = gains[i + 1 - period..=i].iter().sum::<f64>() / period as f64;
let avg_loss: f64 = losses[i + 1 - period..=i].iter().sum::<f64>() / period as f64;
let rsi = if avg_loss == 0.0 {
100.0
} else {
let rs = avg_gain / avg_loss;
100.0 - (100.0 / (1.0 + rs))
};
result.push(Some(rsi));
}
}
result
}
/// 計算所有技術指標
pub fn calculate_all(stock_data: &StockData) -> Vec<TechnicalIndicators> {
let closes: Vec<f64> = stock_data.prices.iter().map(|p| p.close).collect();
let sma_20 = Self::calculate_sma(&closes, 20);
let sma_50 = Self::calculate_sma(&closes, 50);
let ema_12 = Self::calculate_ema(&closes, 12);
let rsi_14 = Self::calculate_rsi(&closes, 14);
(0..closes.len())
.map(|i| TechnicalIndicators {
sma_20: sma_20.get(i).and_then(|&v| v),
sma_50: sma_50.get(i).and_then(|&v| v),
ema_12: ema_12.get(i).and_then(|&v| v),
rsi_14: rsi_14.get(i).and_then(|&v| v),
})
.collect()
}
}
use anyhow::Result;
mod stock;
mod indicators;
use stock::{StockFetcher, StockData};
use indicators::IndicatorCalculator;
#[tokio::main]
async fn main() -> Result<()> {
// 從環境變數讀取 API Key
let api_key = std::env::var("ALPHA_VANTAGE_API_KEY")
.unwrap_or_else(|_| "demo".to_string());
let fetcher = StockFetcher::new(api_key);
// 抓取股價資料
println!("正在抓取 AAPL 股價資料...");
let stock_data = fetcher.fetch_stock_data("AAPL", 100).await?;
println!("成功抓取 {} 筆資料\n", stock_data.prices.len());
// 計算技術指標
let indicators = IndicatorCalculator::calculate_all(&stock_data);
// 顯示最近10天的資料
println!("最近 10 天的股價與技術指標:");
println!("{:-<100}", "");
println!(
"{:<12} {:>10} {:>10} {:>10} {:>10} {:>12} {:>12} {:>10}",
"日期", "開盤", "最高", "最低", "收盤", "SMA(20)", "EMA(12)", "RSI(14)"
);
println!("{:-<100}", "");
let start_idx = stock_data.prices.len().saturating_sub(10);
for (price, indicator) in stock_data.prices[start_idx..]
.iter()
.zip(&indicators[start_idx..])
{
println!(
"{:<12} {:>10.2} {:>10.2} {:>10.2} {:>10.2} {:>12} {:>12} {:>10}",
price.date,
price.open,
price.high,
price.low,
price.close,
indicator.sma_20.map_or("N/A".to_string(), |v| format!("{:.2}", v)),
indicator.ema_12.map_or("N/A".to_string(), |v| format!("{:.2}", v)),
indicator.rsi_14.map_or("N/A".to_string(), |v| format!("{:.2}", v)),
);
}
// 分析最新指標
if let Some(latest) = indicators.last() {
println!("\n{:-<100}", "");
println!("最新技術指標分析:");
if let Some(rsi) = latest.rsi_14 {
println!("RSI(14): {:.2}", rsi);
if rsi > 70.0 {
println!(" → 超買訊號,可能面臨回調");
} else if rsi < 30.0 {
println!(" → 超賣訊號,可能反彈");
} else {
println!(" → 中性區間");
}
}
if let (Some(sma_20), Some(price)) = (
latest.sma_20,
stock_data.prices.last().map(|p| p.close),
) {
let diff_pct = ((price - sma_20) / sma_20) * 100.0;
println!("\n股價與 SMA(20) 差距: {:.2}%", diff_pct);
if diff_pct > 5.0 {
println!(" → 股價顯著高於均線,可能過熱");
} else if diff_pct < -5.0 {
println!(" → 股價顯著低於均線,可能超跌");
}
}
}
Ok(())
}
# 設定 API Key (需要到 Alpha Vantage 註冊)
export ALPHA_VANTAGE_API_KEY=<你的 api key>
# 執行程式
cargo run
寫到現在有點疲倦,好累!完成 21 天